#!/usr/bin/python
# -*- coding: utf-8 -*-

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)


from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: zabbix_action

short_description: Create/Delete/Update Zabbix actions

version_added: "2.8"

description:
    - This module allows you to create, modify and delete Zabbix actions.

author:
    - Ruben Tsirunyan (@rubentsirunyan)
    - Ruben Harutyunov (@K-DOT)

requirements:
    - zabbix-api

options:
    name:
        description:
            - Name of the action
        required: true
    event_source:
        description:
            - Type of events that the action will handle.
        required: true
        choices: ['trigger', 'discovery', 'auto_registration', 'internal']
    state:
        description:
            - State of the action.
            - On C(present), it will create an action if it does not exist or update the action if the associated data is different.
            - On C(absent), it will remove the action if it exists.
        choices: ['present', 'absent']
        default: 'present'
    status:
        description:
            - Status of the action.
        choices: ['enabled', 'disabled']
        default: 'enabled'
    esc_period:
        description:
            - Default operation step duration. Must be greater than 60 seconds. Accepts seconds, time unit with suffix and user macro.
        default: '60'
    conditions:
        type: list
        description:
            - List of dictionaries of conditions to evaluate.
            - For more information about suboptions of this option please
              check out Zabbix API documentation U(https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition)
        suboptions:
            type:
                description: Type (label) of the condition.
                choices:
                    # trigger
                    - host_group
                    - host
                    - trigger
                    - trigger_name
                    - trigger_severity
                    - time_period
                    - host_template
                    - application
                    - maintenance_status
                    - event_tag
                    - event_tag_value
                    # discovery
                    - host_IP
                    - discovered_service_type
                    - discovered_service_port
                    - discovery_status
                    - uptime_or_downtime_duration
                    - received_value
                    - discovery_rule
                    - discovery_check
                    - proxy
                    - discovery_object
                    # auto_registration
                    - proxy
                    - host_name
                    - host_metadata
                    # internal
                    - host_group
                    - host
                    - host_template
                    - application
                    - event_type
            value:
                description:
                    - Value to compare with.
                    - When I(type) is set to C(discovery_status), the choices
                      are C(up), C(down), C(discovered), C(lost).
                    - When I(type) is set to C(discovery_object), the choices
                      are C(host), C(service).
                    - When I(type) is set to C(event_type), the choices
                      are C(item in not supported state), C(item in normal state),
                      C(LLD rule in not supported state),
                      C(LLD rule in normal state), C(trigger in unknown state), C(trigger in normal state).
                    - Besides the above options, this is usualy either the name
                      of the object or a string to compare with.
            operator:
                description:
                    - Condition operator.
                    - When I(type) is set to C(time_period), the choices are C(in), C(not in).
                choices:
                    - '='
                    - '<>'
                    - 'like'
                    - 'not like'
                    - 'in'
                    - '>='
                    - '<='
                    - 'not in'
            formulaid:
                description:
                    - Arbitrary unique ID that is used to reference the condition from a custom expression.
                    - Can only contain upper-case letters.
    formula:
        description:
            - User-defined expression to be used for evaluating conditions of filters with a custom expression.
            - The expression must contain IDs that reference specific filter conditions by its formulaid.
            - The IDs used in the expression must exactly match the ones
              defined in the filter conditions. No condition can remain unused or omitted.
            - Required for custom expression filters.
    default_message:
        description:
            - Problem message default text.
    default_subject:
        description:
            - Problem message default subject.
    recovery_default_message:
        description:
            - Recovery message text.
            - Works only with >= Zabbix 3.2
    recovery_default_subject:
        description:
            - Recovery message subject.
            - Works only with >= Zabbix 3.2
    acknowledge_default_message:
        description:
            - Acknowledge operation message text.
            - Works only with >= Zabbix 3.4
    acknowledge_default_subject:
        description:
            - Acknowledge operation message subject.
            - Works only with >= Zabbix 3.4
    operations:
        type: list
        description:
            - List of action operations
        suboptions:
            type:
                description:
                    - Type of operation.
                choices:
                    - send_message
                    - remote_command
                    - add_host
                    - remove_host
                    - add_to_host_group
                    - remove_from_host_group
                    - link_to_template
                    - unlink_from_template
                    - enable_host
                    - disable_host
                    - set_host_inventory_mode
            esc_period:
                description:
                    - Duration of an escalation step in seconds.
                    - Must be greater than 60 seconds.
                    - Accepts seconds, time unit with suffix and user macro.
                    - If set to 0 or 0s, the default action escalation period will be used.
                default: 0s
            esc_step_from:
                description:
                    - Step to start escalation from.
                default: 1
            esc_step_to:
                description:
                    - Step to end escalation at.
                default: 1
            send_to_groups:
                type: list
                description:
                    - User groups to send messages to.
            send_to_users:
                type: list
                description:
                    - Users to send messages to.
            message:
                description:
                    - Operation message text.
            subject:
                description:
                    - Operation message subject.
            media_type:
                description:
                    - Media type that will be used to send the message.
            host_groups:
                type: list
                description:
                    - List of host groups host should be added to.
                    - Required when I(type=add_to_host_group) or I(type=remove_from_host_group).
            templates:
                type: list
                description:
                    - List of templates host should be linked to.
                    - Required when I(type=link_to_template) or I(type=unlink_from_template).
            inventory:
                description:
                    - Host inventory mode.
                    - Required when I(type=set_host_inventory_mode).
            command_type:
                description:
                    - Type of operation command.
                    - Required when I(type=remote_command).
                choices:
                    - custom_script
                    - ipmi
                    - ssh
                    - telnet
                    - global_script
            command:
                description:
                    - Command to run.
                    - Required when I(type=remote_command) and I(command_type!=global_script).
            execute_on:
                description:
                    - Target on which the custom script operation command will be executed.
                    - Required when I(type=remote_command) and I(command_type=custom_script).
                choices:
                    - agent
                    - server
                    - proxy
            run_on_groups:
                description:
                    - Host groups to run remote commands on.
                    - Required when I(type=remote_command) if I(run_on_hosts) is not set.
            run_on_hosts:
                description:
                    - Hosts to run remote commands on.
                    - Required when I(type=remote_command) if I(run_on_groups) is not set.
                    - If set to 0 the command will be run on the current host.
            ssh_auth_type:
                description:
                    - Authentication method used for SSH commands.
                    - Required when I(type=remote_command) and I(command_type=ssh).
                choices:
                    - password
                    - public_key
            ssh_privatekey_file:
                description:
                    - Name of the private key file used for SSH commands with public key authentication.
                    - Required when I(type=remote_command) and I(command_type=ssh).
            ssh_publickey_file:
                description:
                    - Name of the public key file used for SSH commands with public key authentication.
                    - Required when I(type=remote_command) and I(command_type=ssh).
            username:
                description:
                    - User name used for authentication.
                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
            password:
                description:
                    - Password used for authentication.
                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
            port:
                description:
                    - Port number used for authentication.
                    - Required when I(type=remote_command) and I(command_type in [ssh, telnet]).
            script_name:
                description:
                    - The name of script used for global script commands.
                    - Required when I(type=remote_command) and I(command_type=global_script).
    recovery_operations:
        type: list
        description:
            - List of recovery operations.
            - C(Suboptions) are the same as for I(operations).
            - Works only with >= Zabbix 3.2
    acknowledge_operations:
        type: list
        description:
            - List of acknowledge operations.
            - C(Suboptions) are the same as for I(operations).
            - Works only with >= Zabbix 3.4

notes:
    - Only Zabbix >= 3.0 is supported.


extends_documentation_fragment:
    - zabbix
'''

EXAMPLES = '''
# Trigger action with only one condition
- name: Deploy trigger action
  zabbix_action:
    server_url: "http://zabbix.example.com/zabbix/"
    login_user: Admin
    login_password: secret
    name: "Send alerts to Admin"
    event_source: 'trigger'
    state: present
    status: enabled
    conditions:
      - type: 'trigger_severity'
        operator: '>='
        value: 'Information'
    operations:
      - type: send_message
        subject: "Something bad is happening"
        message: "Come on, guys do something"
        media_type: 'Email'
        send_to_users:
          - 'Admin'

# Trigger action with multiple conditions and operations
- name: Deploy trigger action
  zabbix_action:
    server_url: "http://zabbix.example.com/zabbix/"
    login_user: Admin
    login_password: secret
    name: "Send alerts to Admin"
    event_source: 'trigger'
    state: present
    status: enabled
    conditions:
      - type: 'trigger_name'
        operator: 'like'
        value: 'Zabbix agent is unreachable'
        formulaid: A
      - type: 'trigger_severity'
        operator: '>='
        value: 'disaster'
        formulaid: B
    formula: A or B
    operations:
      - type: send_message
        media_type: 'Email'
        send_to_users:
          - 'Admin'
      - type: remote_command
        command: 'systemctl restart zabbix-agent'
        run_on_hosts:
          - 0

# Trigger action with recovery and acknowledge operations
- name: Deploy trigger action
  zabbix_action:
    server_url: "http://zabbix.example.com/zabbix/"
    login_user: Admin
    login_password: secret
    name: "Send alerts to Admin"
    event_source: 'trigger'
    state: present
    status: enabled
    conditions:
      - type: 'trigger_severity'
        operator: '>='
        value: 'Information'
    operations:
      - type: send_message
        subject: "Something bad is happening"
        message: "Come on, guys do something"
        media_type: 'Email'
        send_to_users:
          - 'Admin'
    recovery_operations:
      - type: send_message
        subject: "Host is down"
        message: "Come on, guys do something"
        media_type: 'Email'
        send_to_users:
          - 'Admin'
    acknowledge_operations:
      - type: send_message
        media_type: 'Email'
        send_to_users:
          - 'Admin'
'''

RETURN = '''
msg:
    description: The result of the operation
    returned: success
    type: str
    sample: 'Action Deleted: Register webservers, ID: 0001'
'''

try:
    from zabbix_api import ZabbixAPI
    HAS_ZABBIX_API = True
except ImportError:
    HAS_ZABBIX_API = False

from ansible.module_utils.basic import AnsibleModule


class Zapi(object):
    """
    A simple wrapper over the Zabbix API
    """
    def __init__(self, module, zbx):
        self._module = module
        self._zapi = zbx

    def check_if_action_exists(self, name):
        """Check if action exists.

        Args:
            name: Name of the action.

        Returns:
            The return value. True for success, False otherwise.

        """
        try:
            _action = self._zapi.action.get({
                "selectOperations": "extend",
                "selectRecoveryOperations": "extend",
                "selectAcknowledgeOperations": "extend",
                "selectFilter": "extend",
                'selectInventory': 'extend',
                'filter': {'name': [name]}
            })
            if len(_action) > 0:
                _action[0]['recovery_operations'] = _action[0].pop('recoveryOperations', [])
                _action[0]['acknowledge_operations'] = _action[0].pop('acknowledgeOperations', [])
            return _action
        except Exception as e:
            self._module.fail_json(msg="Failed to check if action '%s' exists: %s" % (name, e))

    def get_action_by_name(self, name):
        """Get action by name

        Args:
            name: Name of the action.

        Returns:
            dict: Zabbix action

        """
        try:
            action_list = self._zapi.action.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [name]}
            })
            if len(action_list) < 1:
                self._module.fail_json(msg="Action not found: " % name)
            else:
                return action_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get ID of '%s': %s" % (name, e))

    def get_host_by_host_name(self, host_name):
        """Get host by host name

        Args:
            host_name: host name.

        Returns:
            host matching host name

        """
        try:
            host_list = self._zapi.host.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'host': [host_name]}
            })
            if len(host_list) < 1:
                self._module.fail_json(msg="Host not found: %s" % host_name)
            else:
                return host_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get host '%s': %s" % (host_name, e))

    def get_hostgroup_by_hostgroup_name(self, hostgroup_name):
        """Get host group by host group name

        Args:
            hostgroup_name: host group name.

        Returns:
            host group matching host group name

        """
        try:
            hostgroup_list = self._zapi.hostgroup.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [hostgroup_name]}
            })
            if len(hostgroup_list) < 1:
                self._module.fail_json(msg="Host group not found: %s" % hostgroup_name)
            else:
                return hostgroup_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get host group '%s': %s" % (hostgroup_name, e))

    def get_template_by_template_name(self, template_name):
        """Get template by template name

        Args:
            template_name: template name.

        Returns:
            template matching template name

        """
        try:
            template_list = self._zapi.template.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'host': [template_name]}
            })
            if len(template_list) < 1:
                self._module.fail_json(msg="Template not found: %s" % template_name)
            else:
                return template_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get template '%s': %s" % (template_name, e))

    def get_trigger_by_trigger_name(self, trigger_name):
        """Get trigger by trigger name

        Args:
            trigger_name: trigger name.

        Returns:
            trigger matching trigger name

        """
        try:
            trigger_list = self._zapi.trigger.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'description': [trigger_name]}
            })
            if len(trigger_list) < 1:
                self._module.fail_json(msg="Trigger not found: %s" % trigger_name)
            else:
                return trigger_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get trigger '%s': %s" % (trigger_name, e))

    def get_discovery_rule_by_discovery_rule_name(self, discovery_rule_name):
        """Get discovery rule by discovery rule name

        Args:
            discovery_rule_name: discovery rule name.

        Returns:
            discovery rule matching discovery rule name

        """
        try:
            discovery_rule_list = self._zapi.drule.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [discovery_rule_name]}
            })
            if len(discovery_rule_list) < 1:
                self._module.fail_json(msg="Discovery rule not found: %s" % discovery_rule_name)
            else:
                return discovery_rule_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get discovery rule '%s': %s" % (discovery_rule_name, e))

    def get_discovery_check_by_discovery_check_name(self, discovery_check_name):
        """Get discovery check  by discovery check name

        Args:
            discovery_check_name: discovery check name.

        Returns:
            discovery check matching discovery check name

        """
        try:
            discovery_check_list = self._zapi.dcheck.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [discovery_check_name]}
            })
            if len(discovery_check_list) < 1:
                self._module.fail_json(msg="Discovery check not found: %s" % discovery_check_name)
            else:
                return discovery_check_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get discovery check '%s': %s" % (discovery_check_name, e))

    def get_proxy_by_proxy_name(self, proxy_name):
        """Get proxy by proxy name

        Args:
            proxy_name: proxy name.

        Returns:
            proxy matching proxy name

        """
        try:
            proxy_list = self._zapi.proxy.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'host': [proxy_name]}
            })
            if len(proxy_list) < 1:
                self._module.fail_json(msg="Proxy not found: %s" % proxy_name)
            else:
                return proxy_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get proxy '%s': %s" % (proxy_name, e))

    def get_mediatype_by_mediatype_name(self, mediatype_name):
        """Get mediatype by mediatype name

        Args:
            mediatype_name: mediatype name

        Returns:
            mediatype matching mediatype name

        """
        try:
            mediatype_list = self._zapi.mediatype.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'description': [mediatype_name]}
            })
            if len(mediatype_list) < 1:
                self._module.fail_json(msg="Media type not found: %s" % mediatype_name)
            else:
                return mediatype_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get mediatype '%s': %s" % (mediatype_name, e))

    def get_user_by_user_name(self, user_name):
        """Get user by user name

        Args:
            user_name: user name

        Returns:
            user matching user name

        """
        try:
            user_list = self._zapi.user.get({
                'output': 'extend',
                'selectInventory':
                'extend', 'filter': {'alias': [user_name]}
            })
            if len(user_list) < 1:
                self._module.fail_json(msg="User not found: %s" % user_name)
            else:
                return user_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get user '%s': %s" % (user_name, e))

    def get_usergroup_by_usergroup_name(self, usergroup_name):
        """Get usergroup by usergroup name

        Args:
            usergroup_name: usergroup name

        Returns:
            usergroup matching usergroup name

        """
        try:
            usergroup_list = self._zapi.usergroup.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [usergroup_name]}
            })
            if len(usergroup_list) < 1:
                self._module.fail_json(msg="User group not found: %s" % usergroup_name)
            else:
                return usergroup_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get user group '%s': %s" % (usergroup_name, e))

    # get script by script name
    def get_script_by_script_name(self, script_name):
        """Get script by script name

        Args:
            script_name: script name

        Returns:
            script matching script name

        """
        try:
            if script_name is None:
                return {}
            script_list = self._zapi.script.get({
                'output': 'extend',
                'selectInventory': 'extend',
                'filter': {'name': [script_name]}
            })
            if len(script_list) < 1:
                self._module.fail_json(msg="Script not found: %s" % script_name)
            else:
                return script_list[0]
        except Exception as e:
            self._module.fail_json(msg="Failed to get script '%s': %s" % (script_name, e))


class Action(object):
    """
    Restructures the user defined action data to fit the Zabbix API requirements
    """
    def __init__(self, module, zbx, zapi_wrapper):
        self._module = module
        self._zapi = zbx
        self._zapi_wrapper = zapi_wrapper

    def _construct_parameters(self, **kwargs):
        """Contruct parameters.

        Args:
            **kwargs: Arbitrary keyword parameters.

        Returns:
            dict: dictionary of specified parameters
        """
        return {
            'name': kwargs['name'],
            'eventsource': to_numeric_value([
                'trigger',
                'discovery',
                'auto_registration',
                'internal'], kwargs['event_source']),
            'esc_period': kwargs.get('esc_period'),
            'filter': kwargs['conditions'],
            'def_longdata': kwargs['default_message'],
            'def_shortdata': kwargs['default_subject'],
            'r_longdata': kwargs['recovery_default_message'],
            'r_shortdata': kwargs['recovery_default_subject'],
            'ack_longdata': kwargs['acknowledge_default_message'],
            'ack_shortdata': kwargs['acknowledge_default_subject'],
            'operations': kwargs['operations'],
            'recovery_operations': kwargs.get('recovery_operations'),
            'acknowledge_operations': kwargs.get('acknowledge_operations'),
            'status': to_numeric_value([
                'enabled',
                'disabled'], kwargs['status'])
        }

    def check_difference(self, **kwargs):
        """Check difference between action and user specified parameters.

        Args:
            **kwargs: Arbitrary keyword parameters.

        Returns:
            dict: dictionary of differences
        """
        existing_action = convert_unicode_to_str(self._zapi_wrapper.check_if_action_exists(kwargs['name'])[0])
        parameters = convert_unicode_to_str(self._construct_parameters(**kwargs))
        change_parameters = {}
        _diff = cleanup_data(compare_dictionaries(parameters, existing_action, change_parameters))
        return _diff

    def update_action(self, **kwargs):
        """Update action.

        Args:
            **kwargs: Arbitrary keyword parameters.

        Returns:
            action: updated action
        """
        try:
            if self._module.check_mode:
                self._module.exit_json(msg="Action would be updated if check mode was not specified: %s" % kwargs, changed=True)
            kwargs['actionid'] = kwargs.pop('action_id')
            return self._zapi.action.update(kwargs)
        except Exception as e:
            self._module.fail_json(msg="Failed to update action '%s': %s" % (kwargs['actionid'], e))

    def add_action(self, **kwargs):
        """Add action.

        Args:
            **kwargs: Arbitrary keyword parameters.

        Returns:
            action: added action
        """
        try:
            if self._module.check_mode:
                self._module.exit_json(msg="Action would be added if check mode was not specified", changed=True)
            parameters = self._construct_parameters(**kwargs)
            action_list = self._zapi.action.create(parameters)
            return action_list['actionids'][0]
        except Exception as e:
            self._module.fail_json(msg="Failed to create action '%s': %s" % (kwargs['name'], e))

    def delete_action(self, action_id):
        """Delete action.

        Args:
            action_id: Action id

        Returns:
            action: deleted action
        """
        try:
            if self._module.check_mode:
                self._module.exit_json(msg="Action would be deleted if check mode was not specified", changed=True)
            return self._zapi.action.delete([action_id])
        except Exception as e:
            self._module.fail_json(msg="Failed to delete action '%s': %s" % (action_id, e))


class Operations(object):
    """
    Restructures the user defined operation data to fit the Zabbix API requirements
    """
    def __init__(self, module, zbx, zapi_wrapper):
        self._module = module
        # self._zapi = zbx
        self._zapi_wrapper = zapi_wrapper

    def _construct_operationtype(self, operation):
        """Construct operation type.

        Args:
            operation: operation to construct

        Returns:
            str: constructed operation
        """
        try:
            return to_numeric_value([
                "send_message",
                "remote_command",
                "add_host",
                "remove_host",
                "add_to_host_group",
                "remove_from_host_group",
                "link_to_template",
                "unlink_from_template",
                "enable_host",
                "disable_host",
                "set_host_inventory_mode"], operation['type']
            )
        except Exception as e:
            self._module.fail_json(msg="Unsupported value '%s' for operation type." % operation['type'])

    def _construct_opmessage(self, operation):
        """Construct operation message.

        Args:
            operation: operation to construct the message

        Returns:
            dict: constructed operation message
        """
        try:
            return {
                'default_msg': '0' if 'message' in operation or 'subject' in operation else '1',
                'mediatypeid': self._zapi_wrapper.get_mediatype_by_mediatype_name(
                    operation.get('media_type')
                )['mediatypeid'] if operation.get('media_type') is not None else None,
                'message': operation.get('message'),
                'subject': operation.get('subject'),
            }
        except Exception as e:
            self._module.fail_json(msg="Failed to construct operation message. The error was: %s" % e)

    def _construct_opmessage_usr(self, operation):
        """Construct operation message user.

        Args:
            operation: operation to construct the message user

        Returns:
            list: constructed operation message user or None if operation not found
        """
        if operation.get('send_to_users') is None:
            return None
        return [{
            'userid': self._zapi_wrapper.get_user_by_user_name(_user)['userid']
        } for _user in operation.get('send_to_users')]

    def _construct_opmessage_grp(self, operation):
        """Construct operation message group.

        Args:
            operation: operation to construct the message group

        Returns:
            list: constructed operation message group or None if operation not found
        """
        if operation.get('send_to_groups') is None:
            return None
        return [{
            'usrgrpid': self._zapi_wrapper.get_usergroup_by_usergroup_name(_group)['usrgrpid']
        } for _group in operation.get('send_to_groups')]

    def _construct_opcommand(self, operation):
        """Construct operation command.

        Args:
            operation: operation to construct command

        Returns:
            list: constructed operation command
        """
        try:
            return {
                'type': to_numeric_value([
                    'custom_script',
                    'ipmi',
                    'ssh',
                    'telnet',
                    'global_script'], operation.get('command_type', 'custom_script')),
                'command': operation.get('command'),
                'execute_on': to_numeric_value([
                    'agent',
                    'server',
                    'proxy'], operation.get('execute_on', 'server')),
                'scriptid': self._zapi_wrapper.get_script_by_script_name(
                    operation.get('script_name')
                ).get('scriptid'),
                'authtype': to_numeric_value([
                    'password',
                    'private_key'
                ], operation.get('ssh_auth_type', 'password')),
                'privatekey': operation.get('ssh_privatekey_file'),
                'publickey': operation.get('ssh_publickey_file'),
                'username': operation.get('username'),
                'password': operation.get('password'),
                'port': operation.get('port')
            }
        except Exception as e:
            self._module.fail_json(msg="Failed to construct operation command. The error was: %s" % e)

    def _construct_opcommand_hst(self, operation):
        """Construct operation command host.

        Args:
            operation: operation to construct command host

        Returns:
            list: constructed operation command host
        """
        if operation.get('run_on_hosts') is None:
            return None
        return [{
            'hostid': self._zapi_wrapper.get_host_by_host_name(_host)['hostid']
        } if str(_host) != '0' else {'hostid': '0'} for _host in operation.get('run_on_hosts')]

    def _construct_opcommand_grp(self, operation):
        """Construct operation command group.

        Args:
            operation: operation to construct command group

        Returns:
            list: constructed operation command group
        """
        if operation.get('run_on_groups') is None:
            return None
        return [{
            'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['hostid']
        } for _group in operation.get('run_on_groups')]

    def _construct_opgroup(self, operation):
        """Construct operation group.

        Args:
            operation: operation to construct group

        Returns:
            list: constructed operation group
        """
        return [{
            'groupid': self._zapi_wrapper.get_hostgroup_by_hostgroup_name(_group)['groupid']
        } for _group in operation.get('host_groups', [])]

    def _construct_optemplate(self, operation):
        """Construct operation template.

        Args:
            operation: operation to construct template

        Returns:
            list: constructed operation template
        """
        return [{
            'templateid': self._zapi_wrapper.get_template_by_template_name(_template)['templateid']
        } for _template in operation.get('templates', [])]

    def _construct_opinventory(self, operation):
        """Construct operation inventory.

        Args:
            operation: operation to construct inventory

        Returns:
            dict: constructed operation inventory
        """
        return {'inventory_mode': operation.get('inventory')}

    def construct_the_data(self, operations):
        """Construct the operation data using helper methods.

        Args:
            operation: operation to construct

        Returns:
            list: constructed operation data
        """
        constructed_data = []
        for op in operations:
            operation_type = self._construct_operationtype(op)
            constructed_operation = {
                'operationtype': operation_type,
                'esc_period': op.get('esc_period'),
                'esc_step_from': op.get('esc_step_from'),
                'esc_step_to': op.get('esc_step_to')
            }
            # Send Message type
            if constructed_operation['operationtype'] == '0':
                constructed_operation['opmessage'] = self._construct_opmessage(op)
                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)

            # Send Command type
            if constructed_operation['operationtype'] == '1':
                constructed_operation['opcommand'] = self._construct_opcommand(op)
                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)

            # Add to/Remove from host group
            if constructed_operation['operationtype'] in ('4', '5'):
                constructed_operation['opgroup'] = self._construct_opgroup(op)

            # Link/Unlink template
            if constructed_operation['operationtype'] in ('6', '7'):
                constructed_operation['optemplate'] = self._construct_optemplate(op)

            # Set inventory mode
            if constructed_operation['operationtype'] == '10':
                constructed_operation['opinventory'] = self._construct_opinventory(op)

            constructed_data.append(constructed_operation)

        return cleanup_data(constructed_data)


class RecoveryOperations(Operations):
    """
    Restructures the user defined recovery operations data to fit the Zabbix API requirements
    """
    def _construct_operationtype(self, operation):
        """Construct operation type.

        Args:
            operation: operation to construct type

        Returns:
            str: constructed operation type
        """
        try:
            return to_numeric_value([
                "send_message",
                "remote_command",
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                "notify_all_involved"], operation['type']
            )
        except Exception as e:
            self._module.fail_json(msg="Unsupported value '%s' for recovery operation type." % operation['type'])

    def construct_the_data(self, operations):
        """Construct the recovery operations data using helper methods.

        Args:
            operation: operation to construct

        Returns:
            list: constructed recovery operations data
        """
        if operations is None:
            return None
        constructed_data = []
        for op in operations:
            operation_type = self._construct_operationtype(op)
            constructed_operation = {
                'operationtype': operation_type,
            }

            # Send Message type
            if constructed_operation['operationtype'] in ('0', '11'):
                constructed_operation['opmessage'] = self._construct_opmessage(op)
                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)

            # Send Command type
            if constructed_operation['operationtype'] == '1':
                constructed_operation['opcommand'] = self._construct_opcommand(op)
                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)

            constructed_data.append(constructed_operation)

        return cleanup_data(constructed_data)


class AcknowledgeOperations(Operations):
    """
    Restructures the user defined acknowledge operations data to fit the Zabbix API requirements
    """
    def _construct_operationtype(self, operation):
        """Construct operation type.

        Args:
            operation: operation to construct type

        Returns:
            str: constructed operation type
        """
        try:
            return to_numeric_value([
                "send_message",
                "remote_command",
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                "notify_all_involved"], operation['type']
            )
        except Exception as e:
            self._module.fail_json(msg="Unsupported value '%s' for acknowledge operation type." % operation['type'])

    def construct_the_data(self, operations):
        """Construct the acknowledge operations data using helper methods.

        Args:
            operation: operation to construct

        Returns:
            list: constructed acknowledge operations data
        """
        if operations is None:
            return None
        constructed_data = []
        for op in operations:
            operation_type = self._construct_operationtype(op)
            constructed_operation = {
                'operationtype': operation_type,
            }

            # Send Message type
            if constructed_operation['operationtype'] in ('0', '11'):
                constructed_operation['opmessage'] = self._construct_opmessage(op)
                constructed_operation['opmessage_usr'] = self._construct_opmessage_usr(op)
                constructed_operation['opmessage_grp'] = self._construct_opmessage_grp(op)

            # Send Command type
            if constructed_operation['operationtype'] == '1':
                constructed_operation['opcommand'] = self._construct_opcommand(op)
                constructed_operation['opcommand_hst'] = self._construct_opcommand_hst(op)
                constructed_operation['opcommand_grp'] = self._construct_opcommand_grp(op)

            constructed_data.append(constructed_operation)

        return cleanup_data(constructed_data)


class Filter(object):
    """
    Restructures the user defined filter conditions to fit the Zabbix API requirements
    """
    def __init__(self, module, zbx, zapi_wrapper):
        self._module = module
        self._zapi = zbx
        self._zapi_wrapper = zapi_wrapper

    def _construct_evaltype(self, _eval, _conditions):
        """Construct the eval type

        Args:
            _eval: zabbix condition evaluation formula
            _conditions: list of conditions to check

        Returns:
            dict: constructed acknowledge operations data
        """
        if len(_conditions) <= 1:
            return {
                'evaltype': '0',
                'formula': None
            }
        return {
            'evaltype': '3',
            'formula': _eval
        }

    def _construct_conditiontype(self, _condition):
        """Construct the condition type

        Args:
            _condition: condition to check

        Returns:
            str: constructed condition type data
        """
        try:
            return to_numeric_value([
                "host_group",
                "host",
                "trigger",
                "trigger_name",
                "trigger_severity",
                "trigger_value",
                "time_period",
                "host_ip",
                "discovered_service_type",
                "discovered_service_port",
                "discovery_status",
                "uptime_or_downtime_duration",
                "received_value",
                "host_template",
                None,
                "application",
                "maintenance_status",
                None,
                "discovery_rule",
                "discovery_check",
                "proxy",
                "discovery_object",
                "host_name",
                "event_type",
                "host_metadata",
                "event_tag",
                "event_tag_value"], _condition['type']
            )
        except Exception as e:
            self._module.fail_json(msg="Unsupported value '%s' for condition type." % _condition['type'])

    def _construct_operator(self, _condition):
        """Construct operator

        Args:
            _condition: condition to construct

        Returns:
            str: constructed operator
        """
        try:
            return to_numeric_value([
                "=",
                "<>",
                "like",
                "not like",
                "in",
                ">=",
                "<=",
                "not in"], _condition['operator']
            )
        except Exception as e:
            self._module.fail_json(msg="Unsupported value '%s' for operator." % _condition['operator'])

    def _construct_value(self, conditiontype, value):
        """Construct operator

        Args:
            conditiontype: type of condition to construct
            value: value to construct

        Returns:
            str: constructed value
        """
        try:
            # Host group
            if conditiontype == '0':
                return self._zapi_wrapper.get_hostgroup_by_hostgroup_name(value)['groupid']
            # Host
            if conditiontype == '1':
                return self._zapi_wrapper.get_host_by_host_name(value)['hostid']
            # Trigger
            if conditiontype == '2':
                return self._zapi_wrapper.get_trigger_by_trigger_name(value)['triggerid']
            # Trigger name: return as is
            # Trigger severity
            if conditiontype == '4':
                return to_numeric_value([
                    "not classified",
                    "information",
                    "warning",
                    "average",
                    "high",
                    "disaster"], value or "not classified"
                )

            # Trigger value
            if conditiontype == '5':
                return to_numeric_value([
                    "ok",
                    "problem"], value or "ok"
                )
            # Time period: return as is
            # Host IP: return as is
            # Discovered service type
            if conditiontype == '8':
                return to_numeric_value([
                    "SSH",
                    "LDAP",
                    "SMTP",
                    "FTP",
                    "HTTP",
                    "POP",
                    "NNTP",
                    "IMAP",
                    "TCP",
                    "Zabbix agent",
                    "SNMPv1 agent",
                    "SNMPv2 agent",
                    "ICMP ping",
                    "SNMPv3 agent",
                    "HTTPS",
                    "Telnet"], value
                )
            # Discovered service port: return as is
            # Discovery status
            if conditiontype == '10':
                return to_numeric_value([
                    "up",
                    "down",
                    "discovered",
                    "lost"], value
                )
            if conditiontype == '13':
                return self._zapi_wrapper.get_template_by_template_name(value)['templateid']
            if conditiontype == '18':
                return self._zapi_wrapper.get_discovery_rule_by_discovery_rule_name(value)['druleid']
            if conditiontype == '19':
                return self._zapi_wrapper.get_discovery_check_by_discovery_check_name(value)['dcheckid']
            if conditiontype == '20':
                return self._zapi_wrapper.get_proxy_by_proxy_name(value)['proxyid']
            if conditiontype == '21':
                return to_numeric_value([
                    "pchldrfor0",
                    "host",
                    "service"], value
                )
            if conditiontype == '23':
                return to_numeric_value([
                    "item in not supported state",
                    "item in normal state",
                    "LLD rule in not supported state",
                    "LLD rule in normal state",
                    "trigger in unknown state",
                    "trigger in normal state"], value
                )
            return value
        except Exception as e:
            self._module.fail_json(
                msg="""Unsupported value '%s' for specified condition type.
                       Check out Zabbix API documetation for supported values for
                       condition type '%s' at
                       https://www.zabbix.com/documentation/3.4/manual/api/reference/action/object#action_filter_condition""" % (value, conditiontype)
            )

    def construct_the_data(self, _formula, _conditions):
        """Construct the user defined filter conditions to fit the Zabbix API
        requirements operations data using helper methods.

        Args:
            _formula:  zabbix condition evaluation formula
            _conditions: conditions to construct

        Returns:
            dict: user defined filter conditions
        """
        if _conditions is None:
            return None
        constructed_data = {}
        constructed_data['conditions'] = []
        for cond in _conditions:
            condition_type = self._construct_conditiontype(cond)
            constructed_data['conditions'].append({
                "conditiontype": condition_type,
                "value": self._construct_value(condition_type, cond.get("value")),
                "value2": cond.get("value2"),
                "formulaid": cond.get("formulaid"),
                "operator": self._construct_operator(cond)
            })
        _constructed_evaltype = self._construct_evaltype(
            _formula,
            constructed_data['conditions']
        )
        constructed_data['evaltype'] = _constructed_evaltype['evaltype']
        constructed_data['formula'] = _constructed_evaltype['formula']
        return cleanup_data(constructed_data)


def convert_unicode_to_str(data):
    """Converts unicode objects to strings in dictionary
    args:
        data: unicode object

    Returns:
        dict: strings in dictionary
    """
    if isinstance(data, dict):
        return dict(map(convert_unicode_to_str, data.items()))
    elif isinstance(data, (list, tuple, set)):
        return type(data)(map(convert_unicode_to_str, data))
    elif data is None:
        return data
    else:
        return str(data)


def to_numeric_value(strs, value):
    """Converts string values to integers
    Args:
        value: string value

    Returns:
        int: converted integer
    """
    strs = [s.lower() if isinstance(s, str) else s for s in strs]
    value = value.lower()
    tmp_dict = dict(zip(strs, list(range(len(strs)))))
    return str(tmp_dict[value])


def compare_lists(l1, l2, diff_dict):
    """
    Compares l1 and l2 lists and adds the items that are different
    to the diff_dict dictionary.
    Used in recursion with compare_dictionaries() function.
    Args:
        l1: first list to compare
        l2: second list to compare
        diff_dict: dictionary to store the difference

    Returns:
        dict: items that are different
    """
    if len(l1) != len(l2):
        diff_dict.append(l1)
        return diff_dict
    for i, item in enumerate(l1):
        if isinstance(item, dict):
            diff_dict.insert(i, {})
            diff_dict[i] = compare_dictionaries(item, l2[i], diff_dict[i])
        else:
            if item != l2[i]:
                diff_dict.append(item)
    while {} in diff_dict:
        diff_dict.remove({})
    return diff_dict


def compare_dictionaries(d1, d2, diff_dict):
    """
    Compares d1 and d2 dictionaries and adds the items that are different
    to the diff_dict dictionary.
    Used in recursion with compare_lists() function.
    Args:
        d1: first dictionary to compare
        d2: second ditionary to compare
        diff_dict: dictionary to store the difference

    Returns:
        dict: items that are different
    """
    for k, v in d1.items():
        if k not in d2:
            diff_dict[k] = v
            continue
        if isinstance(v, dict):
            diff_dict[k] = {}
            compare_dictionaries(v, d2[k], diff_dict[k])
            if diff_dict[k] == {}:
                del diff_dict[k]
            else:
                diff_dict[k] = v
        elif isinstance(v, list):
            diff_dict[k] = []
            compare_lists(v, d2[k], diff_dict[k])
            if diff_dict[k] == []:
                del diff_dict[k]
            else:
                diff_dict[k] = v
        else:
            if v != d2[k]:
                diff_dict[k] = v
    return diff_dict


def cleanup_data(obj):
    """Removes the None values from the object and returns the object
    Args:
        obj: object to cleanup

    Returns:
       object: cleaned object
    """
    if isinstance(obj, (list, tuple, set)):
        return type(obj)(cleanup_data(x) for x in obj if x is not None)
    elif isinstance(obj, dict):
        return type(obj)((cleanup_data(k), cleanup_data(v))
                         for k, v in obj.items() if k is not None and v is not None)
    else:
        return obj


def main():
    """Main ansible module function
    """
    module = AnsibleModule(
        argument_spec=dict(
            server_url=dict(type='str', required=True, aliases=['url']),
            login_user=dict(type='str', required=True),
            login_password=dict(type='str', required=True, no_log=True),
            http_login_user=dict(type='str', required=False, default=None),
            http_login_password=dict(type='str', required=False, default=None, no_log=True),
            validate_certs=dict(type='bool', required=False, default=True),
            esc_period=dict(type='int', required=False, default=60),
            timeout=dict(type='int', default=10),
            name=dict(type='str', required=True),
            event_source=dict(type='str', required=True, choices=['trigger', 'discovery', 'auto_registration', 'internal']),
            state=dict(type='str', required=False, default='present', choices=['present', 'absent']),
            status=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']),
            default_message=dict(type='str', required=False, default=None),
            default_subject=dict(type='str', required=False, default=None),
            recovery_default_message=dict(type='str', required=False, default=None),
            recovery_default_subject=dict(type='str', required=False, default=None),
            acknowledge_default_message=dict(type='str', required=False, default=None),
            acknowledge_default_subject=dict(type='str', required=False, default=None),
            conditions=dict(type='list', required=False, default=None),
            formula=dict(type='str', required=False, default=None),
            operations=dict(type='list', required=False, default=None),
            recovery_operations=dict(type='list', required=False, default=[]),
            acknowledge_operations=dict(type='list', required=False, default=[])
        ),
        supports_check_mode=True
    )

    if not HAS_ZABBIX_API:
        module.fail_json(msg="Missing required zabbix-api module (check docs or install with: pip install zabbix-api)")

    server_url = module.params['server_url']
    login_user = module.params['login_user']
    login_password = module.params['login_password']
    http_login_user = module.params['http_login_user']
    http_login_password = module.params['http_login_password']
    validate_certs = module.params['validate_certs']
    timeout = module.params['timeout']
    name = module.params['name']
    esc_period = module.params['esc_period']
    event_source = module.params['event_source']
    state = module.params['state']
    status = module.params['status']
    default_message = module.params['default_message']
    default_subject = module.params['default_subject']
    recovery_default_message = module.params['recovery_default_message']
    recovery_default_subject = module.params['recovery_default_subject']
    acknowledge_default_message = module.params['acknowledge_default_message']
    acknowledge_default_subject = module.params['acknowledge_default_subject']
    conditions = module.params['conditions']
    formula = module.params['formula']
    operations = module.params['operations']
    recovery_operations = module.params['recovery_operations']
    acknowledge_operations = module.params['acknowledge_operations']

    try:
        zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user,
                        passwd=http_login_password, validate_certs=validate_certs)
        zbx.login(login_user, login_password)
    except Exception as e:
        module.fail_json(msg="Failed to connect to Zabbix server: %s" % e)

    zapi_wrapper = Zapi(module, zbx)

    action = Action(module, zbx, zapi_wrapper)

    action_exists = zapi_wrapper.check_if_action_exists(name)
    ops = Operations(module, zbx, zapi_wrapper)
    recovery_ops = RecoveryOperations(module, zbx, zapi_wrapper)
    acknowledge_ops = AcknowledgeOperations(module, zbx, zapi_wrapper)
    fltr = Filter(module, zbx, zapi_wrapper)

    if action_exists:
        action_id = zapi_wrapper.get_action_by_name(name)['actionid']
        if state == "absent":
            result = action.delete_action(action_id)
            module.exit_json(changed=True, msg="Action Deleted: %s, ID: %s" % (name, result))
        else:
            difference = action.check_difference(
                action_id=action_id,
                name=name,
                event_source=event_source,
                esc_period=esc_period,
                status=status,
                default_message=default_message,
                default_subject=default_subject,
                recovery_default_message=recovery_default_message,
                recovery_default_subject=recovery_default_subject,
                acknowledge_default_message=acknowledge_default_message,
                acknowledge_default_subject=acknowledge_default_subject,
                operations=ops.construct_the_data(operations),
                recovery_operations=recovery_ops.construct_the_data(recovery_operations),
                acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations),
                conditions=fltr.construct_the_data(formula, conditions)
            )

            if difference == {}:
                module.exit_json(changed=False, msg="Action is up to date: %s" % (name))
            else:
                result = action.update_action(
                    action_id=action_id,
                    **difference
                )
                module.exit_json(changed=True, msg="Action Updated: %s, ID: %s" % (name, result))
    else:
        if state == "absent":
            module.exit_json(changed=False)
        else:
            action_id = action.add_action(
                name=name,
                event_source=event_source,
                esc_period=esc_period,
                status=status,
                default_message=default_message,
                default_subject=default_subject,
                recovery_default_message=recovery_default_message,
                recovery_default_subject=recovery_default_subject,
                acknowledge_default_message=acknowledge_default_message,
                acknowledge_default_subject=acknowledge_default_subject,
                operations=ops.construct_the_data(operations),
                recovery_operations=recovery_ops.construct_the_data(recovery_operations),
                acknowledge_operations=acknowledge_ops.construct_the_data(acknowledge_operations),
                conditions=fltr.construct_the_data(formula, conditions)
            )
            module.exit_json(changed=True, msg="Action created: %s, ID: %s" % (name, action_id))


if __name__ == '__main__':
    main()
